/// <summary>
/// Copyright 2010 Ivo Limmen (ivo@limmen.org)
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
/// 
/// http://www.apache.org/licenses/LICENSE-2.0
/// 
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
/// </summary>

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using System.Text;
using System.Linq;

using ServiceController.Api;
using ServiceController.Exceptions;
using ServiceController.Logging;
using Services.Configuration;
using Services.Configuration.Api;
using Services.Logging.Api;
using Util.Reflection;

namespace ServiceController
{
    public class ServiceController : IServiceManager
    {
        private static ServiceController current = null;

        private List<IService> services = new List<IService>();

        private IResolver localConfiguration = null;

        private ILogger logger = null;

        private int remotingPort = 1519;

        private string loggingCategory = "ServiceController";

        private ILogService logService;

        private List<String> includePaths = new List<string>();

        public ServiceController()
        {
            if (current != null)
            {
                return;
            }

            // Configure
            ConfigureLocalConfiguration();

            current = this;

            List<IService> loaded = new List<IService>();
            LoadServices(loaded, AppDomain.CurrentDomain.BaseDirectory);
            LoadExtraServices(loaded);

            RegisterServices(loaded);

            InitializeServices();
        }

        private void LoadExtraServices(List<IService> services)
        {
            foreach (string path in this.includePaths)
            {
                GetLogger().Info("Scanning for services in [" + path + "]");
                LoadServices(services, path);
            }
        }

        private void LoadServices(List<IService> loaded, string path)
        {
            ImplementationFinder serviceFinder = new ImplementationFinder(path, true);

            foreach (IService service in serviceFinder.GetImplementations<IService>())
            {
                if (!loaded.Select(s => s.GetName()).Contains(service.GetName()))
                {
                    GetLogger().Info(string.Format("Service [{0}] discovered on [{1}] is loaded.", service.GetName(), path));
                    loaded.Add(service);
                }
                else
                {
                    GetLogger().Warn(string.Format("Service [{0}] discovered on [{1}] is allready loaded and is ignored.", service.GetName(), path));
                }
            }
        }

        private void ConfigureLocalConfiguration()
        {
            ConfigurationLoader loader = new ConfigurationLoader(AppDomain.CurrentDomain.BaseDirectory, "service-controller");
            ConfigurationReader reader = new ConfigurationReader(loader);

            this.localConfiguration = reader.Read();

            remotingPort = Int32.Parse(this.localConfiguration.GetSetting(null, "remoting", "port"));

            String includePath = this.localConfiguration.GetSetting(null, "services", "include-path");

            if (!string.IsNullOrEmpty(includePath))
            {
                includePath = includePath.Replace('\n', ' ');
                includePath = includePath.Replace('\r', ' ');
                includePath = includePath.Trim();

                foreach (string path in includePath.Split(new char[] { ';' }))
                {
                    string newPath = path.Trim();

                    if (Directory.Exists(newPath))
                    {
                        this.includePaths.Add(newPath);
                    }
                    else
                    {
                        throw new IncludePathDoesNotExist(newPath + " does not exist.");
                    }
                }
            }
        }

        public static ServiceController GetInstance()
        {
            return current;
        }

        #region IServiceManager implementation

        public List<IService> GetServices()
        {
            // return a defensive copy
            return new List<IService>(this.services);
        }

        public IService GetServiceByName(string name)
        {
            foreach (IService service in this.services)
            {
                if (service.GetName().Equals(name))
                {
                    return service;
                }
            }

            return null;
        }

        #endregion

        internal ILogger GetLogger()
        {
            if (this.logger == null)
            {
                if (this.logService == null)
                {
                    this.logService = (ILogService)GetServiceByName("LogService");
                }
                if (this.logService != null)
                {
                    this.logger = logService.GetLogger(loggingCategory);
                }
            }
            if (this.logger == null)
            {
                return new StandardOutLogger(loggingCategory);
            }

            return this.logger;
        }

        private void RegisterServices(List<IService> services)
        {
            if (services == null || services.Count == 0)
            {
                throw new NoServicesAvailable();
            }

            GetLogger().Info("Registering services...");

            TcpChannel channel = new TcpChannel(this.remotingPort);
            ChannelServices.RegisterChannel(channel, false);
            int registered = 0;

            while (services.Count > 0)
            {
                registered = 0;

                for (int index = 0; index < services.Count; index++)
                {
                    IService service = services[index];

                    if (DependenciesResolved(service))
                    {
                        RegisterService(service);
                        registered++;

                        services.RemoveAt(index);
                    }
                }

                if (registered == 0 && services.Count > 0)
                {
                    StringBuilder names = new StringBuilder();

                    foreach (IService service in services)
                    {
                        names.Append(service.GetName());
                        names.Append(", ");
                    }

                    names = names.Remove(names.Length - 2, 2);

                    throw new DependencyNotResolvableException(string.Format("Services '{0}' could not be started due to unresolved dependencies...", names));
                }
            }
        }

        private void InitializeServices()
        {
            // make sure each service is able to retrieve other services
            foreach (IService service in this.services)
            {
                service.SetServiceManager(this);
                service.SetLogger(this.logService.GetLogger(service.GetName()));
                service.SetConfiguration(this.localConfiguration);
            }

            // initialize all services
            foreach (IService service in this.services)
            {
                service.Initialize();
            }

            GetLogger().Info("All services have been initialized...");
        }

        public void StopServices()
        {
            // stop all services
            foreach (IService service in this.services)
            {
                service.Stop();
            }
        }

        private bool DependenciesResolved(IService service)
        {
            if (service.GetRequiredServices() == null || service.GetRequiredServices().Length == 0)
            {
                return true;
            }

            bool resolved = true;

            foreach (string serviceName in service.GetRequiredServices())
            {
                if (this.GetServiceByName(serviceName) == null)
                {
                    resolved = false;
                    break;
                }
            }

            return resolved;
        }

        private void RegisterService(IService service)
        {
            if (!service.GetType().Equals(typeof(BaseService)) && service.RequiresRemoting())
            {
                try
                {
                    RemotingConfiguration.RegisterWellKnownServiceType(service.GetType(), service.GetName(), WellKnownObjectMode.Singleton);

                    this.services.Add(service);

                    GetLogger().Info(service.GetName() + " started...");
                }
                catch (Exception e)
                {
                    GetLogger().Error(e.Message);
                }
            }
        }
    }
}
